#include "xbelld.h"

#include <errno.h>
#include <signal.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <unistd.h>
#include <math.h>

#ifdef HAVE_ALSA
# include <alsa/asoundlib.h>
#endif

#include <X11/XKBlib.h>

char usage[] = "\
X daemon that performs an action every time the bell is rung.\n\
\n\
USAGE\n\
    xbelld [bD] [-t N] [-d duration] [-F freq] [-v vol] \n\
    xbelld [bDc] [-t N] -f file\n\
    xbelld [bD] [-t N] -e cmd...\n\
    xbelld -h\n\
\n\
OPTIONS\n\
    -b		Run in background\n\
    -D		Don't disable audio bell on startup\n\
    -t N	Interval (ms) during which subsequent bells are throttled\n\
    -T		Play a test bell on startup.\n\
    -C		Use a more complex waveform than a pure sine wave.\n\
    -c		Cache audio file in memory\n\
    -f file	Name of file to play when bell is rung\n\
    -e cmd ...	Command to execute when bell is rung. (Must be last option)\n\
    -d dur	Beep duration (ms)\n\
    -F freq	Beep frequency (hz)\n\
    -v vol	Beep volume (0 -- 100).\n\
    -h		Print this help and exit\
";

/*
 * Option variables
 */
#ifdef HAVE_ALSA
double      (*waveform)(double param)  = sin;
# ifndef NO_WAVE
char	    *filename	    	= NULL;
# endif
int	    bell_volume,	    /* Max 100, min 0x00 */
	    bell_duration;	    /* Milliseconds */
float	    bell_freq;		    /* Frequency in Hz */
#endif

int	    disable_bell    	= 1,
	    test_bell		= 0,
	    suppress_interval	= 0;
char	    **cmd		= NULL;

#if defined(HAVE_ALSA) && !defined( NO_WAVE )
/*
 * Cached sound file info
 */
int	    cchannels	= 0,
	    crate    	= 0,
	    cformat    	= SND_PCM_FORMAT_UNKNOWN,
	    csize    	= 0;
char	    *cbuffer	= NULL;
#endif /* defined(HAVE_ALSA) && !defined( NO_WAVE ) */

/*
 * Prototypes
 */
void		ringBell		();
#ifdef HAVE_ALSA
double          complexWave		(double param);
int		playSineWave		();
# ifndef NO_WAVE
int		playSoundFile		();
int		playSoundBuffer		();
int		readWaveHeader		( FILE *f,
					  int *channels_ret,
					  int *rate_ret,
					  int *format_ret,
					  int *size_ret );
# endif
#endif


void
die(const char *fmt, ...)
{
    va_list	    ap;

    fflush( stdout );

    va_start( ap, fmt);

    fprintf( stderr, "(FATAL ERROR) " APL_NAME ": " );
    vfprintf( stderr, fmt, ap );
    fprintf( stderr, "\n" );

    va_end( ap );
    exit(1);

    /* NOT REACHED */
}


void
message( const char *fmt, ... )
{
    va_list	    ap;

    fflush( stdout );

    va_start( ap, fmt);

    fprintf( stderr, APL_NAME ": " );
    vfprintf( stderr, fmt, ap );
    fprintf( stderr, "\n" );

    va_end( ap );
}

void processOptions( int argc, char **argv )
{
    int	    opt;
#if defined( HAVE_ALSA ) && !defined( NO_WAVE )
    int	    cache_sound = 0;
#endif /* defined( HAVE_ALSA ) && !defined( NO_WAVE ) */

    /* Process options */
    while( ( opt = getopt(argc, argv, "bcCd:Def:F:ht:Tv:") ) != -1 )
    {
	switch(opt)
	{
	    case 'b':	/* Run in background */
		switch( fork() )
		{
		    case -1:
			message( "Error forking %s", strerror(errno) );
			/* Fall through */

		    case 0: /* Child thread */
			break;

		    default: /* Parent thread */
			exit( EXIT_SUCCESS );
		}
		break;

	    case 'D':	/* Don't disable audio bell */
		disable_bell = 0;
		break;

	    case 'T':	/* Test bell on startup */
		test_bell = 1;
		break;

	    case 'h':
		printf( "xbelld version %s.\n\n", VERSION );
		puts( usage );
		exit( EXIT_SUCCESS );
		/* Not reached */

	    case 't':
		suppress_interval = strtoul( optarg, NULL, 0 );
		break;

	    case 'e':
		cmd = argv + optind;
		return;
		/* Not reached */

#ifdef HAVE_ALSA
# ifndef NO_WAVE
	    case 'f':	/* Filename of bell sound */
		free( filename );
		filename = strdup( optarg );
		break;

	    case 'c':	/* Cache sound file */
		cache_sound = 1;
		break;

	    case 'C':	/* Complex waveform */
		waveform = complexWave;
		break;
# endif /* NO_WAVE */

	    case 'd':	/* Bell duration */
		bell_duration = strtoul( optarg, NULL, 0 );
		break;

	    case 'F':	/* Bell frequency */
		bell_freq = strtof( optarg, NULL );
		break;

	    case 'v':	/* Bell volume */
		bell_volume = strtoul( optarg, NULL, 0 );
		if( bell_volume > 100 )
		    bell_volume = 100;
		break;
#endif

	    default:
		die( "Bad usage" );
	}
    }

#ifndef HAVE_ALSA
    if( cmd == NULL )
	die( "No command given." );
#endif

#if defined( HAVE_ALSA ) && !defined( NO_WAVE )
    if( filename != NULL && cache_sound )
    {
	FILE *f;
	int bytes_read;

	errno = 0;	/* Clear errors */

	if( (f = fopen( filename, "r" )) == NULL )
	    die( "fopen( %s, r ): %s", filename, strerror( errno ) );

	if( ! readWaveHeader( f, &cchannels, &crate, &cformat, &csize ) )
	    die( "Error reading WAVE file header" );

	if( (cbuffer = (char*) malloc( csize )) == NULL )
	    die( "malloc( %d ): %s", csize, strerror( errno ) );

	bytes_read = fread( cbuffer, 1, csize, f );
	if( bytes_read != csize )
	{
	    message( "Short read -- Read %d bytes, expected %d",
		    bytes_read, csize );
	    csize = bytes_read;
	}
	if( csize == 0 )
	    die( "Zero bytes in cached buffer" );

	fclose( f );

	debug( 1, "Cached audio file %s (%d bytes)", filename, csize );
    }


    debug( 1, "Options: disable_bell=%d, suppress_interval=%d, test_bell=%d\n"
	    "cache=%d filename=%s cmd=%s duration=%d freq=%.2f vol=%d",
	    disable_bell, suppress_interval, test_bell, cache_sound, filename,
	    cmd ? *cmd : "(nil)", bell_duration, bell_freq, bell_volume );
#endif /* defined( HAVE_ALSA ) && !defined( NO_WAVE ) */
}

int main(int argc, char **argv)
{
    Display	*dpy;
    int		mjr, mnr, xkb_event_code, xkb_error;
    XkbEvent	ev;

    struct sigaction	act;
    struct timeval	last_bell = {0,0};

    /*
     * Set signal masks. Dead children are not waitpid()'d, so make sure they
     * don't become zombies.
     */
    act.sa_flags = SA_NOCLDWAIT;
    act.sa_handler = SIG_IGN;
    sigemptyset( &act.sa_mask );
    sigaction( SIGCHLD, &act, NULL );

    mjr = XkbMajorVersion;
    mnr = XkbMinorVersion;
    dpy = XkbOpenDisplay( NULL, &xkb_event_code, NULL, &mjr, &mnr, &xkb_error);
    if( dpy == NULL )
    {
	switch (xkb_error)
	{
	    case XkbOD_BadLibraryVersion:
		die( "Require Xkb version %d.%02d (got library %d.%02d)",
			XkbMajorVersion, XkbMinorVersion, mjr, mnr );
		/* NOT REACHED */

	    case XkbOD_ConnectionRefused:
		die( "Cannot open display" );
		/* NOT REACHED */

	    case XkbOD_NonXkbServer:
		die( "XKB extension not present" );
		/* NOT REACHED */

	    case XkbOD_BadServerVersion:
		die( "Require Xkb version %d.%02d (got server %d.%02d)",
			XkbMajorVersion, XkbMinorVersion, mjr, mnr );
		/* NOT REACHED */

	    default:
		die( "Unknown error %d from XkbOpenDisplay", xkb_error);
		/* NOT REACHED */
	}
    }

#ifdef HAVE_ALSA
    /* Get default bell parameters from X */
    {
	XKeyboardState state;
	if( XGetKeyboardControl( dpy, &state ) )
	{
	    bell_volume   = state.bell_percent;
	    bell_freq     = state.bell_pitch;
	    bell_duration = state.bell_duration;

	    if( bell_volume == 0 ) bell_volume = 80;
	}

	debug( 0, "Got volume=%d, freq=%.2f, duration=%d from X server",
		bell_volume, bell_freq, bell_duration );
    }
#endif

    processOptions( argc, argv );

    XkbSelectEvents( dpy, XkbUseCoreKbd, XkbBellNotifyMask, XkbBellNotifyMask );

    if( disable_bell )
    {
	debug( 1, "Disabling audible bell" );
	if(
		!XkbChangeEnabledControls( dpy, XkbUseCoreKbd,
		    XkbAudibleBellMask, 0 )
	  )
	    die( "Couldn't disable audible bell" );
    }

    if( test_bell )
    {
	debug( 0, "Testing bell" );
	ringBell();
    }

    for(;;)
    {
	struct timeval now;

	XNextEvent( dpy, &ev.core );
	debug( 2, "Got event type=%d. (xkb_event_code=%d)", ev.type, xkb_event_code );
	if( ev.type == xkb_event_code )
	{
	    if( suppress_interval > 0 )
	    {
		int time_since_last_bell;

		gettimeofday( &now, NULL );

		time_since_last_bell = 1000 * (now.tv_sec - last_bell.tv_sec) +
		    (now.tv_usec - last_bell.tv_usec) / 1000;

		debug( 9, "Time since last bell: %d", time_since_last_bell );
		if(
			last_bell.tv_sec == 0 ||
			time_since_last_bell > suppress_interval
		  )
		    ringBell();

		last_bell = now;

	    } /* if( max_bells_per_sec > 0 ) */

	    else
		ringBell();

	} /* if( ev.type == xkb_event_code ) */

    } /* for(;;) */

    XCloseDisplay( dpy );
    return 0;
}

void
ringBell()
{
    debug( 1, "%s()", __func__ );

    pid_t   pid = fork();

    switch( pid )
    {
	case -1: /* Fork error */
	    message( "Error calling fork(): %s", strerror(errno) );
	    if( cmd != NULL )
		exit( EXIT_FAILURE );
	    /* Else fall through */

	case 0: /* Child thread */
	    if( cmd != NULL )
	    {
		execvp( *cmd, cmd );
		message( "Error executing %s: %s", *cmd, strerror(errno) );
		exit( EXIT_FAILURE ); /* Kill child thread */
	    }

#ifdef HAVE_ALSA
	    else
	    {
		int status;
		
# ifndef NO_WAVE
		if( cbuffer )
		    status = playSoundBuffer();
		else if ( filename )
		    status = playSoundFile();
		else
# endif /* NO_WAVE */
		    status = playSineWave();

		if( pid == 0 )
		{
		    /* If we seceded calling fork() kill the child. */ 
		    debug( 3, "Killing child thread (PID=%d, success=%d)",
			    pid, status );
		    exit( status ? EXIT_SUCCESS : EXIT_FAILURE );
		}
		else
		    break;
	    }
	    /* Not reached */
#endif


	default: /* Parent thread */
	    debug( 3, "Returning from %s(). (child pid=%d)", __func__, pid );
	    break;
    }
}

double complexWave(double param)
{
    /*
     * This is sin(x) + sin(3x)/sqrt(3) + ... + sin(9x)/sqrt(9), which
     * after a little experimentation seems to be a nice mellow beep
     * with neither the piercing quality of a pure sine wave nor the
     * loud harshness of a square wave.
     */
    return (sin(param) +
            sin(3*param) * 0.192450089729875254836382926833 +
            sin(5*param) * 0.089442719099991587856366946749 +
            sin(7*param) * 0.053994924715603889602073790890 +
            sin(9*param) * 0.037037037037037037037037037037);

}

#ifdef HAVE_ALSA
int
playSineWave()
{
    int		status, i;
    snd_pcm_t	*handle;

    const int	rate = 44100, /* CD quality */
		bufsiz = BUFSIZ / sizeof(int16_t);
    int16_t	buffer[bufsiz];

    int nsamples;
    float jmult;

    debug( 3, "%s()", __func__ );

    errno = 0;	/* Clear errors */

    status = snd_pcm_open( &handle, "default", SND_PCM_STREAM_PLAYBACK, 0 );
    if( status < 0)
    {
	message( "Playback open error: %s", snd_strerror( status ) );
	goto fail;
    }

    status = snd_pcm_set_params( handle, SND_PCM_FORMAT_S16,
	    SND_PCM_ACCESS_RW_INTERLEAVED,
	    1,		/* Mono */
	    rate,
	    1,		/* soft_resample */
	    500000);	/* latency (us). */
    if( status < 0 )
    {
	message( "Playback open error: %s", snd_strerror(status) );
	goto close_all_and_fail;
    }

    if( snd_pcm_frames_to_bytes( handle, 1 ) != sizeof( *buffer ) )
	die( "Expect %lu bytes per frame. Got %lu.",
		sizeof( *buffer ), snd_pcm_frames_to_bytes( handle, 1 ) );

    nsamples = rate * bell_duration / 1000;
    jmult = 2 * M_PI * bell_freq / rate;

    for( i=0; i < nsamples; i += bufsiz )
    {
	int j;
	int jmax = i + bufsiz < nsamples ? i + bufsiz : nsamples;
	float ampl = INT16_MAX * bell_volume / 100.;

	snd_pcm_sframes_t frames;

	/* Put a sine wave in buffer */
	for( j=i; j < jmax; j++ )
	    buffer[j-i] =  ampl * waveform( jmult  * j );

#if defined(DEBUG) && DEBUG_LEVEL > 0
	if( i == 0 )
	{
	    fprintf( stderr, "Sine wave: " );
	    for( j=0; j < 2 * M_PI / jmult; j++ )
		fprintf( stderr, "%d ", buffer[j-i] );
	    fputc( '\n', stderr );
	}
#endif /* defined(DEBUG) && DEBUG_LEVEL > 0 */

	/* Write buffer to sound device */
	frames = snd_pcm_writei(handle, buffer, jmax - i );
	if( frames < 0 )
	    frames = snd_pcm_recover( handle, frames, 0 );

	if (frames < 0)
	{
	    message( "snd_pcm_writei failed: %s", snd_strerror(frames) );
	    goto close_all_and_fail;
	}

	if( frames > 0 && frames < jmax - i )
	    message( "Short write (expected %d, wrote %li)",
		    jmax - i, frames);

    } /* for( i )*/

    snd_pcm_drain( handle );
    snd_pcm_close( handle );
    return 1; /* Success */

close_all_and_fail:
    snd_pcm_close( handle );

fail:
    return 0;
}

# ifndef NO_WAVE
int
playSoundBuffer()
{
    int			status;
    snd_pcm_t		*handle;
    snd_pcm_sframes_t	frames;
    int			cframes;

    debug( 3, "%s()", __func__ );

    errno = 0;	/* Clear errors */

    status = snd_pcm_open( &handle, "default", SND_PCM_STREAM_PLAYBACK, 0 );
    if( status < 0)
    {
	message( "Playback open error: %s", snd_strerror( status ) );
	goto fail;
    }

    status = snd_pcm_set_params( handle, cformat, SND_PCM_ACCESS_RW_INTERLEAVED,
	    cchannels, crate,
	    1,		/* soft_resample */
	    0);		/* latency (us). Was 500000 (0.5sec) */
    if( status < 0 )
    {
	message( "Playback open error: %s", snd_strerror(status) );
	goto close_all_and_fail;
    }

    cframes = snd_pcm_bytes_to_frames( handle, csize );
    debug( 1, "Writing %d frames to sound device", cframes );
    frames = snd_pcm_writei(handle, cbuffer, cframes );
    if( frames < 0 )
	frames = snd_pcm_recover( handle, frames, 0 );

    if (frames < 0)
    {
	message( "snd_pcm_writei failed: %s", snd_strerror(frames) );
	goto close_all_and_fail;
    }

    if( frames > 0 && frames < cframes )
	message( "Short write (expected %d, wrote %li)",
		cframes, frames);

    snd_pcm_drain( handle );
    snd_pcm_close( handle );
    return 1; /* Success */

close_all_and_fail:
    snd_pcm_close( handle );

fail:
    return 0;
}

int
playSoundFile()
{
    char    buffer[BUFSIZ];
    FILE    *f;
    int	    channels, rate, format;

    int			status;
    snd_pcm_t		*handle;
    snd_pcm_sframes_t	frames;

    debug( 3, "%s()", __func__ );

    errno = 0;	/* Clear errors */

    if( (f = fopen( filename, "r" )) == NULL )
    {
	message( "fopen( %s, r ): %s", filename, strerror( errno ) );
	goto close_f_and_fail;
    }

    if( ! readWaveHeader( f, &channels, &rate, &format, NULL ) )
	die( "Error reading WAVE file header" );

    status = snd_pcm_open( &handle, "default", SND_PCM_STREAM_PLAYBACK, 0 );
    if( status < 0)
    {
	message( "Playback open error: %s", snd_strerror( status ) );
	goto close_f_and_fail;
    }

    status = snd_pcm_set_params( handle, format, SND_PCM_ACCESS_RW_INTERLEAVED,
	    channels, rate,
	    1,		/* soft_resample */
	    0);		/* latency (us). Was 500000 (0.5sec) */
    if( status < 0 )
    {
	message( "Playback open error: %s", snd_strerror(status) );
	goto close_all_and_fail;
    }

    while( !feof(f) )
    {
	int bytes_read, frames_read;

	bytes_read = fread( buffer, 1, sizeof(buffer), f );
	if( bytes_read == 0 )
	{
	    message( "fread(): %m" );
	    goto close_all_and_fail;
	}
	frames_read = snd_pcm_bytes_to_frames( handle, bytes_read );

	debug( 9, "Writing %d frames to sound device", frames_read );
	frames = snd_pcm_writei(handle, buffer, frames_read );
	if( frames < 0 )
	    frames = snd_pcm_recover( handle, frames, 0 );

	if (frames < 0)
	{
	    message( "snd_pcm_writei failed: %s", snd_strerror(frames) );
	    goto close_all_and_fail;
	}

	if( frames > 0 && frames < frames_read )
	    message( "Short write (expected %d, wrote %li)",
		    bytes_read, frames);
    }

    snd_pcm_drain( handle );
    snd_pcm_close( handle );
    fclose(f);
    return 1; /* Success */

close_all_and_fail:
    snd_pcm_close( handle );

close_f_and_fail:
    fclose(f);
    return 0;
}

/*
 * Sets channels / frequency / format from wave header if present. Then
 * does an fseek() to the wave data. If it fails, it rewinds and returns.
 */
int
readWaveHeader(
	FILE *f, int *channels_ret, int *rate_ret, int *format_ret,
	int *size_ret)
{
    wave_header_t	header;
    int			format;

    errno = 0;	/* Clear error */

    if( fread( &header, sizeof(header), 1, f ) != 1 )
	goto fail;

    /* Check for magic bytes in header */
    if(
	    header.magic != COMPOSE_ID( 'R', 'I', 'F', 'F' ) ||
	    header.type != COMPOSE_ID( 'W', 'A', 'V', 'E' )
      )
    {
	debug( 1, "No wave header: magic=%.4s, type=%.4s",
		(char*) &header.magic, (char*) &header.type );
	goto fail;
    }

    /* Swap some endian, if needed. Only swap fields we read. */
    header.fmt_len = LE_INT(header.fmt_len);
    header.pcm_code = LE_SHORT(header.pcm_code);
    header.channels = LE_SHORT(header.channels);
    header.rate = LE_INT(header.rate);
    header.bit_pspl = LE_SHORT(header.bit_pspl);
    header.data_len = LE_INT(header.data_len);

    debug( 2, "Header: magic=%.4s, type=%.4s, fmt=%.4s, fmt_len=0x%x, "
	    "pcm_code=0x%x data=%.4s", (char*) &header.magic,
	    (char*) &header.type, (char*) &header.fmt, header.fmt_len,
	    header.pcm_code, (char*) &header.data );
    if(
	    header.fmt != COMPOSE_ID( 'f', 'm', 't', ' ' ) ||
	    header.fmt_len != 0x10 ||
	    header.pcm_code != 0x01 ||
	    header.data != COMPOSE_ID( 'd', 'a', 't', 'a' )
      )
    {
	die( "Unrecognized WAVE file. Try and encode it with\n"
		"mplayer -vo null -vc null -ao pcm:fast:file=out.wav <file>" );
    }

    if( header.channels <= 0 )
	die( "Cannot play file with %d channels", header.channels );

    switch( header.bit_pspl )
    {
	case 8:
	    format = SND_PCM_FORMAT_U8;
	    break;

	case 16:
	    format = SND_PCM_FORMAT_S16_LE;
	    break;

	case 24:
	    switch( header.byte_pspl / header.channels )
	    {
		case 3:
		    format = SND_PCM_FORMAT_S24_3LE;
		    break;

		case 4:
		    format = SND_PCM_FORMAT_S24_LE;
		    break;

		default:
		    die( "Cannot play file with %d channels, "
			    "%d bytes per sample and %d bits per sample",
			    header.channels, header.byte_pspl,
			    header.bit_pspl );
	    }
	    break;

	case 32:
	    format = SND_PCM_FORMAT_S32_LE;
	    break;

	default:
	    die( "Cannot play file which is %d bits wide", header.bit_pspl );
    }

    debug( 2, "WAVE header options: channels=%d, rate=%d, format=%d",
	    header.channels, header.rate, format );

    *format_ret = format;
    *channels_ret = header.channels;
    *rate_ret = header.rate;
    if( size_ret )
	*size_ret = header.data_len;

    return 1;

fail:
    if( errno )
    {
	message( "%s(): %s", __func__, strerror( errno ) );
	errno = 0;
    }

    *format_ret = SND_PCM_FORMAT_UNKNOWN;
    rewind(f);

    return 0;
}
# endif /* NO_WAVE */
#endif /* HAVE_ALSA */
